package lang;

import editor.NotImplementedException;
import editor.widgets.CustomProgressBar;
import lang.asm.ASM;
import model.Project;
import model.Segment;
import model.code.*;
import model.symbol.Constant;
import model.symbol.LocalVariable;
import model.symbol.StructureMember;
import observer.ObservedString;
import util.*;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

/**
 * This is the superclass of all language loading classes.
 *
 * Loading differs from parsing in that loading is done on entire files to locate and identify {@link CodeFileChunk}s, whereas parsing is only within the content of {@link CodeFileChunk}s.
 *
 * @author Neil Dickson
 */
public abstract class Loader {
	private static final Directory WINDOWS_INCLUDES_DIRECTORY = new Directory("includes"+File.separator+"windows","",null,null);

	protected abstract boolean splitLinesFirst();
	public abstract void initialScan(FileLoadingData data,ArrayList<FileLoadingData> allData, Project project);
	public abstract void constantScan(FileLoadingData data,ArrayList<FileLoadingData> allData, Project project);
	public abstract void globalScan(FileLoadingData data,ArrayList<FileLoadingData> allData, Project project);
	public abstract void structureScan(FileLoadingData data,ArrayList<FileLoadingData> allData, Project project);
	public abstract void contentScan(FileLoadingData data,ArrayList<FileLoadingData> allData, Project project);
	public abstract void docScan(FileLoadingData data,ArrayList<FileLoadingData> allData, Project project);

	public static final HashMap<String,Language> EXTENSION_TO_LANGUAGE_MAP = new HashMap<String,Language>();
	public static final HashMap<String,Language> NAME_TO_LANGUAGE_MAP = new HashMap<String,Language>();
	public static final String LIBRARY_INCLUDE_EXTENSION = ".pwni";

	public static void loadAPIFile(CodeFile file) throws IOException {
		byte[] text = FileUtil.readFile(file.getFilePath());
		Constants constants = null;
		// Skip over the name of the original file
		int i = ASUtil.indexOf(text,'\n',0)+1;
		StringBuffer fileDesc = new StringBuffer();
		while (text[i]==';') {
			++i;
			int endOfLine = ASUtil.indexOf(text,'\n',i);
			if (fileDesc.length()!=0) {
				fileDesc.append('\n');
			}
			fileDesc.append(ASUtil.subCharArray(text,i,endOfLine));
			i = endOfLine+1;
		}
		file.getDescription().replaceAll(fileDesc.toString());
		while (i<text.length) {
			byte c = text[i];
			// Structure
			if (c=='{') {
				int endOfLine = ASUtil.indexOf(text,'\n',i);
				String name = ASUtil.substring(text,i+1,endOfLine);
				i = endOfLine+1;
				String description = "";
				while (text[i]==';') {
					endOfLine = ASUtil.indexOf(text,'\n',i);
					description += ASUtil.substring(text,i+1,endOfLine);
					i = endOfLine+1;
				}
				Structure structure = new Structure(name,description);
				file.add(structure);
				while (text[i]!='}') {
					int tabIndex = ASUtil.indexOf(text,'\t',i);
					String type = ASUtil.substring(text,i,tabIndex);
					int tabIndex2 = ASUtil.indexOf(text,'\t',tabIndex+1);
					String memberName = ASUtil.substring(text,tabIndex+1,tabIndex2);
					boolean isStructure = text[tabIndex2+1]=='<';
					endOfLine = ASUtil.indexOf(text,'\n',tabIndex2);
					// Check for and handle any array length number
					boolean hasLength = !(tabIndex2+2>=endOfLine || text[tabIndex2+2]<'0' || text[tabIndex2+2]>'9');
					int length = 1;
					if (hasLength) {
						length = text[tabIndex2+2]-'0';
						int endOfLength = tabIndex2+3;
						while (endOfLength<endOfLine && text[endOfLength]>='0' && text[endOfLength]<='9') {
							length = length*10 + text[endOfLength]-'0';
							++endOfLength;
						}
						tabIndex2 = endOfLength-2;
					}
					int commentIndex = tabIndex2+2;	//+1 to skip over the tab, and +1 to skip over '<' or '?'
					for (;commentIndex<endOfLine && text[commentIndex]!=';';++commentIndex) {}
					String memberDesc = (commentIndex==endOfLine)?"":ASUtil.substring(text,commentIndex+1,endOfLine);
					structure.addMember(new StructureMember(memberName,type,(hasLength?(length+" dup ("):"")+(isStructure?"<>":"?")+(hasLength?")":""),memberDesc,structure,structure.getMembers().size()));
					i = endOfLine+1;
				}
				i+=2;	// Skip over the '}' and the '\n'
			}
			// Function
			else if (c=='(') {
				int endOfLine = ASUtil.indexOf(text,'\n',i);
				String name = ASUtil.substring(text,i+1,endOfLine);
				i = endOfLine+1;
				String description = "";
				while (text[i]==';') {
					endOfLine = ASUtil.indexOf(text,'\n',i);
					description += ASUtil.substring(text,i+1,endOfLine);
					i = endOfLine+1;
				}
				// Just give it any language
				Function function = new Function(name,description,ASM.language);
				file.add(function);
				while (text[i]!=')') {
					int tabIndex = ASUtil.indexOf(text,'\t',i);
					String type = ASUtil.substring(text,i,tabIndex);
					endOfLine = ASUtil.indexOf(text,'\n',tabIndex);
					int commentIndex = tabIndex+1;
					for (;commentIndex<endOfLine && text[commentIndex]!=';';++commentIndex) {}
					String memberName = ASUtil.substring(text,tabIndex+1,commentIndex);
					String memberDesc = (commentIndex==endOfLine)?"":ASUtil.substring(text,commentIndex+1,endOfLine);
					function.addParameter(new LocalVariable(memberName,type,memberDesc,function,CodeFileChunk.LOCATION_PARAMETERS,function.getParameters().size()));
					i = endOfLine+1;
				}
				i+=2;	// Skip over the ')' and the '\n'
			}
			// Constant
			else {
				int equalsIndex = ASUtil.indexOf(text,'=',i);
				int commentIndex = equalsIndex+1;
				for (;text[commentIndex]!=';' && text[commentIndex]!='\n';++commentIndex) {}
				int endIndex = commentIndex;
				if (text[commentIndex]==';') {
					++commentIndex;
					for (++endIndex;text[endIndex]!='\n';++endIndex) {}
				}
				if (constants==null) {
					constants = new Constants("","");
					file.add(constants);
				}
				Constant constant = new Constant(ASUtil.substring(text,i,equalsIndex),ASUtil.substring(text,commentIndex,endIndex),ASUtil.substring(text,equalsIndex+1,commentIndex),constants,constants.getMembers().size());
				constants.add(constant);
				i = endIndex+1;
			}
		}
	}
	/**
	 * Loads a single file into the specified project
	 * NOTE: This is not for initial project loading; this is for adding a single file.
	 * @param project the project to load the file into
	 * @param file the file to be loaded
	 * @param progress the progress bar to be updated during loading, or null if none
	 * @throws IOException when the file can't be found or read
	 */
	public static void loadFile(Project project,CodeFile file,CustomProgressBar progress) throws IOException {
		ArrayList<CodeFile> files = new ArrayList<CodeFile>(1);
		files.add(file);
		loadFiles(project,files,progress);
	}
	public static void loadFiles(Project project,ArrayList<CodeFile> files,CustomProgressBar progress) throws IOException {
		final boolean isEmptyProject = project.getDirectory().getAllFiles().isEmpty();
		MemoryUtil.gc();
		final long free0 = Runtime.getRuntime().freeMemory();
		final long total0 = Runtime.getRuntime().totalMemory();
		System.out.println("Memory: "+(total0-free0)+"/"+total0);
		System.out.println("ObservedStrings: "+ ObservedString.INSTANCE_COUNT);
		long startTime = System.currentTimeMillis();
		System.out.println("Starting Load of "+files.size()+" files");
		if (isEmptyProject) {
			// Disable notification in the global scope to avoid tons of repeated updates.
			project.getScope().setNotificationEnabled(false);
		}

		ArrayList<String> errorLog = new ArrayList<String>();

		if (progress!=null) {
			progress.setStatus("Loading code files from disk...",progress.getCompletion());
		}
		ArrayList<FileLoadingData> allData = new ArrayList<FileLoadingData>();
		for (CodeFile file : files) {
			allData.add(new FileLoadingData(file,errorLog));
		}
		System.out.println("Start of Load Done "+(System.currentTimeMillis()-startTime));

		// TODO: Do the loading in parallel after ensuring safe mutual exclusion.
		// NOTE: Can't use Iterator/foreach loop for initialScan, because it may add files to allData.
		for (int i=0;i<allData.size();++i) {
			allData.get(i).file.getLanguage().getLoader().initialScan(allData.get(i),allData,project);
//			MemoryUtil.logMemory();
			MemoryUtil.gc();
//			MemoryUtil.logMemory();
		}
		System.out.println("Initial Pass Done "+(System.currentTimeMillis()-startTime));
		if (progress!=null) {
			progress.setStatus("Loading external includes...",0.07f);
		}
		for (Project.ExternalInclude include : project.getImportedIncludes()) {
			String name = include.getName().toString();
			if (name.indexOf('.')>=0) {
				name = name.substring(0, name.indexOf('.'));
			}
			CodeFile incFile = new CodeFile(name+LIBRARY_INCLUDE_EXTENSION, ASM.language, WINDOWS_INCLUDES_DIRECTORY, true, true);
			try {
				loadAPIFile(incFile);
				project.getScope().addAll(incFile.getAllGlobalSymbols());
				project.updateNewCodeFile(incFile);
			}
			catch (FileNotFoundException e) {
				// TODO: Add an error in the library includes UI component when that's supported.
			}
		}
		for (Project.ExternalInclude include : project.getLibraryIncludes()) {
			CodeFile incFile = new CodeFile(include.getName().toString() + ".pwni", ASM.language, WINDOWS_INCLUDES_DIRECTORY, true, true);
			try {
				loadAPIFile(incFile);
				project.getScope().addAll(incFile.getAllGlobalSymbols());
				project.updateNewCodeFile(incFile);
			}
			catch (FileNotFoundException e) {
				// TODO: Add an error in the library includes UI component when that's supported.
			}
		}
		System.out.println("Includes Load Done "+(System.currentTimeMillis()-startTime));
		if (progress!=null) {
			progress.setStatus("Finding constants and structures...",0.07f);
		}
		for (FileLoadingData data : allData) {
			data.file.getLanguage().getLoader().constantScan(data,allData,project);
//			MemoryUtil.logMemory();
			MemoryUtil.gc();
//			MemoryUtil.logMemory();
		}
		System.out.println("Constants Pass Done "+(System.currentTimeMillis()-startTime));
		if (progress!=null) {
			progress.setStatus("Finding functions and variables...",0.08f);
		}
		for (FileLoadingData data : allData) {
			data.file.getLanguage().getLoader().globalScan(data,allData,project);
//			MemoryUtil.logMemory();
			MemoryUtil.gc();
//			MemoryUtil.logMemory();
		}
		System.out.println("Globals Pass Done "+(System.currentTimeMillis()-startTime));
		if (progress!=null) {
			progress.setStatus("Parsing content...",0.09f);
		}
		for (FileLoadingData data : allData) {
			data.file.getLanguage().getLoader().structureScan(data,allData,project);
//			MemoryUtil.logMemory();
			MemoryUtil.gc();
//			MemoryUtil.logMemory();
		}
		System.out.println("Structures Pass Done "+(System.currentTimeMillis()-startTime));
		for (FileLoadingData data : allData) {
			data.file.getLanguage().getLoader().contentScan(data,allData,project);
//			MemoryUtil.logMemory();
			MemoryUtil.gc();
//			MemoryUtil.logMemory();
		}
		System.out.println("Content Pass Done "+(System.currentTimeMillis()-startTime));
		if (progress!=null) {
			progress.setStatus("Finding metadata and documentation...",0.17f);
		}
		for (FileLoadingData data : allData) {
			data.file.getLanguage().getLoader().docScan(data,allData,project);
//			MemoryUtil.logMemory();
			MemoryUtil.gc();
//			MemoryUtil.logMemory();
		}
		System.out.println("Doc Pass Done "+(System.currentTimeMillis()-startTime));
		if (progress!=null) {
			progress.setStatus("Organizing file contents...",0.18f);
		}
		// Lastly, remove all remaining UnparsedText
		for (FileLoadingData data : allData) {
			for (Iterator it = data.content.iterator();it.hasNext();) {
				Object o = it.next();
				if (o instanceof UnparsedText) {
					it.remove();
				}
			}
		}
		MemoryUtil.gc();
		MemoryUtil.logMemory();
		System.out.println("Stripping Unparsed Done "+(System.currentTimeMillis()-startTime));

		for (FileLoadingData data : allData) {
			final CodeFile file = data.file;
			Section currentSection = file;
			Segment currentSegment = null;
			for (Object o : data.getContent()) {
				if (o instanceof Section) {
					Section newSection = (Section)o;
					if (currentSegment!=null) {	// This happens when a segment hasn't ended and a new section starts.
						newSection.getSegmentName().setValue(currentSegment.getName().toString());
					}
					file.add(newSection);
					currentSection = newSection;
				}
				else if (o instanceof Segment) {
					Segment newSegment = (Segment)o;
					if (currentSegment==null) {
						if (currentSection!=file && currentSection!=null && currentSection.getContent().isEmpty()) {
							currentSection.getSegmentName().setValue(newSegment.getName().toString());
						}
						else {
							Section newSection = new Section("","");
							newSection.getSegmentName().setValue(newSegment.getName().toString());
							file.add(newSection);
							currentSection = newSection;
						}
						currentSegment = newSegment;
						project.addSegment(newSegment);
					}
					else if (currentSegment==newSegment) {
						// NOTE: This check is so that the section doesn't become null if it just started and the previous section had a different, default segment.
						//       In that case, the end of that previous segment actually occurs after the start of this section, but the segment about to start should be made the segment for this section instead.
						if (currentSection!=null && !currentSection.getContent().isEmpty()) {
							currentSection = null;
						}
						currentSegment = null;
					}
					else {
						throw new NotImplementedException("Mismatched segment start/finish found and not handled in "+file.getName()+": segments "+currentSegment+" and "+newSegment);
					}
				}
				else if (o instanceof CodeFileChunk) {
					if (currentSection==null) {	// This happens when a segment ends and a new section hasn't started yet.
						currentSection = new Section("","");
					}
					currentSection.add((CodeFileChunk)o);
					// NOTE: It may be premature to update Constants and Structures here, but if they don't get reparsed before Functions and GlobalVariables are notified, things can go wrong.
					//       e.g. MyStructure in MyFile.inc has one member of type DWORD, and in MyFile.asm, something of type MyStructure is copied as a DWORD; there will be an error on that line unless the line is updated after the structure is reparsed.
					//       Structures would normally get reparsed by updateNewCodeFile below, but MyFile.inc comes after MyFile.asm, so it wouldn't be updated soon enough.
					if (o instanceof Constants) {
						((Constants)o).nowHaveGlobalScope();
					}
					else if (o instanceof Structure) {
						((Structure)o).nowHaveGlobalScope();
					}
				}
				else {
					throw new NotImplementedException("An object of type "+o.getClass()+" was found while loading file data.\nPlease report this, because this is an unintentional omission that must be fixed.");
				}
			}
		}
		System.out.println("Creating CodeFiles Done "+(System.currentTimeMillis()-startTime));
		if (progress!=null) {
			progress.setStatus("Reparsing from changes to scope...",0.31f);
		}

		if (isEmptyProject) {
			// Re-enable notification in the global scope so that now global items can get updates.
			project.getScope().setNotificationEnabled(true);
		}
		for (int i = 0; i<allData.size(); i++) {
			project.updateNewCodeFile(allData.get(i).file);
			if (progress!=null) {
				progress.setCompletion(0.31f+((0.95f-0.31f)*(i+1))/allData.size());
			}
		}
		System.out.println("Updating Scope Done "+(System.currentTimeMillis()-startTime));
		for (String error : errorLog) {
			System.err.println(error);
		}
		allData = null;
		MemoryUtil.gc();

		final long free1 = Runtime.getRuntime().freeMemory();
		final long total1 = Runtime.getRuntime().totalMemory();
		System.out.println("Memory: "+(total1-free1)/1048576f+"MB/"+total1/1048576f+"MB");
		System.out.println("Increase: "+((total1-free1)-(total0-free0))/1048576f+"MB or "+(total1-total0)/1048576+"MB");
		System.out.println("ObservedStrings: "+ ObservedString.INSTANCE_COUNT);
	}

	public static class FileLoadingData {
		CodeFile file;
		ArrayList<Object> content;
		ArrayList<String> errorLog;
		ArrayList<Pair<Structure,UnparsedText>> structuresToParse;
		ArrayList<Prototypes> prototypesToCheck;
		FileLoadingData(CodeFile file,ArrayList<String> errorLog) throws IOException {
			String[] text = {FileUtil.readFileString(file.getFilePath())};
			if (file.getLanguage().getLoader().splitLinesFirst()) {
				text = StringUtil.getLines(text[0]);
			}
			content = new ArrayList<Object>();
			content.add(text);
			this.file = file;
			structuresToParse = new ArrayList<Pair<Structure, UnparsedText>>();
			prototypesToCheck = new ArrayList<Prototypes>();
			this.errorLog = errorLog;
		}
		public CodeFile getFile() {
			return file;
		}
		public ArrayList<Object> getContent() {
			return content;
		}

		public void setContent(ArrayList<Object> content) {
			this.content = content;
		}
		public ArrayList<String> getLog() {
			return errorLog;
		}

		public void addToStructureList(Structure structure, UnparsedText structureContent) {
			structuresToParse.add(new Pair<Structure, UnparsedText>(structure,structureContent));
		}
		public ArrayList<Pair<Structure, UnparsedText>> getStructuresToParse() {
			return structuresToParse;
		}
		public ArrayList<Prototypes> getPrototypeList() {
			return prototypesToCheck;
		}
	}
	public static class UnparsedText {
		public String[]	code;
		public String[]	comments;
		public int		startLineNumber;

		public UnparsedText(String[] code, String[] comments, int startLineNumber) {
			this.code = code;
			this.comments = comments;
			this.startLineNumber = startLineNumber;
		}

		public int length() {
			return code.length;
		}

		public UnparsedText getSubrange(int start,int end) {
			return new UnparsedText((String[])ArrayUtil.subarray(code,start,end),(String[])ArrayUtil.subarray(comments,start,end),startLineNumber+start);
		}
	}
}
